package cn.xf.districonfig.client.zk;

import cn.xf.districonfig.client.scope.RefreshScopeRegistry;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.CuratorCache;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;

import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CuratorUtil implements ApplicationContextAware, DisposableBean {

    private Logger logger = LoggerFactory.getLogger(CuratorUtil.class);

    /**
     * zk客户端
     */
    private static CuratorFramework curatorFramework;

    @Value("${zk.client.addr:127.0.0.1:2181}")
    private String connectStr;

    private static String path = "/configProp";

    private ApplicationContext applicationContext;

    private String zkName= "zkSources";

    private ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();

    private ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, new ThreadFactoryBuilder().setNameFormat("thread-schedule-%d").setDaemon(true).build());

    private BeanDefinitionRegistry beanDefinitionRegistry;

    @PostConstruct
    public void init() {
        logger.info("init start");
        RefreshScopeRegistry refreshScopeRegistry = applicationContext.getBean(RefreshScopeRegistry.class);
        beanDefinitionRegistry = refreshScopeRegistry.getBeanDefinitionRegistry();
        curatorFramework = CuratorFrameworkFactory.builder()
                .connectString(connectStr)
                .sessionTimeoutMs(40)
                .retryPolicy(new ExponentialBackoffRetry(1000, 3))
                .build();
        curatorFramework.start();
        try {
            Stat stat = curatorFramework.checkExists().forPath(path);
            if (stat == null) {
                curatorFramework.create()
                        .creatingParentContainersIfNeeded()
                        .withMode(CreateMode.PERSISTENT)
                        .forPath(path, "zk-config".getBytes());
                TimeUnit.SECONDS.sleep(4);
            } else {
                //加载属性值到spring
                //curatorFramework.g
                addToProperty(curatorFramework, path);
            }
        } catch (Exception e) {
            logger.error("Here is an exception：", e);
        }
        scheduledExecutorService.scheduleWithFixedDelay(() -> childCache(curatorFramework, path), 0 ,30, TimeUnit.SECONDS);
        logger.info("init end");
    }

    private void childCache(CuratorFramework client, String path) {
        logger.info("childrenRefresh");
        Environment environment = applicationContext.getEnvironment();
        if (!(environment instanceof ConfigurableEnvironment)) {
            return;
        }
        ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) environment;
        MutablePropertySources propertySources = configurableEnvironment.getPropertySources();
        /* ConcurrentHashMap<String, String> mapR = new ConcurrentHashMap<>();
        try {
            List<String> propers = client.getChildren().forPath(path);
            for (String key : propers) {
                byte[] bytes = client.getData().forPath(path + "/" + key);
                mapR.put(key, bytes != null ? new String(bytes, StandardCharsets.UTF_8) : "");
            }
        } catch (Exception e) {
            logger.error("Here is an exception：", e);
        }*/

        CuratorCache pathChildrenCache = CuratorCache.build(client, path, CuratorCache.Options.COMPRESSED_DATA);
        try {
            pathChildrenCache.start();
            pathChildrenCache.listenable().addListener((type, oldData, data) -> {
                if (type == null) {
                    return;
                }
                switch (type) {
                    case NODE_CHANGED:
                        updateEnv(propertySources, data, client);
                        break;
                    case NODE_CREATED:
                        addEnv(propertySources, data, client);
                        break;
                    case NODE_DELETED:
                        delEnv(propertySources, data, client);
                        break;
                    default:
                        break;
                }
            });
        } catch (Exception e) {
            logger.error("Here is an exception：", e);
        }
        logger.info("childrenRefresh end");
    }

    private void delEnv(MutablePropertySources propertySources, ChildData data, CuratorFramework client) {

        String path = data.getPath();
        String key = path.substring(path.length() + 1);

        PropertySource<?> propertySource = propertySources.get(zkName);
        ConcurrentHashMap<String, String> zkMap = (ConcurrentHashMap<String, String>) propertySource.getSource();

        zkMap.remove(key);
        refreshBean();
    }

    private void updateEnv(MutablePropertySources propertySources, ChildData data, CuratorFramework client) {
        String path = data.getPath();
        String key = path.substring(path.length() + 1);
        String value = data.getData() != null ? new String(data.getData(), StandardCharsets.UTF_8) : "";
        PropertySource<?> propertySource = propertySources.get(zkName);
        ConcurrentHashMap<String, String> zkMap = (ConcurrentHashMap<String, String>) propertySource.getSource();

        zkMap.put(key, value);
        refreshBean();
    }

    private void addEnv(MutablePropertySources propertySources, ChildData data, CuratorFramework client) {
        String path = data.getPath();
        String key = path.substring(path.length() + 1);
        String value = data.getData() != null ? new String(data.getData(), StandardCharsets.UTF_8) : "";
        PropertySource<?> propertySource = propertySources.get(zkName);
        ConcurrentHashMap<String, String> zkMap = (ConcurrentHashMap<String, String>) propertySource.getSource();

        zkMap.put(key, value);
        refreshBean();
    }

    private void refreshBean() {
        String[] beanDefinitionNames = applicationContext.getBeanDefinitionNames();
        for (String beanName : beanDefinitionNames) {

            BeanDefinition beanDefinition = beanDefinitionRegistry.getBeanDefinition(beanName);
            applicationContext.getAutowireCapableBeanFactory().destroyBean(beanDefinition);
            applicationContext.getBean(beanName);
        }
    }

    private void addToProperty(CuratorFramework client, String path) {

        if (!chectExistsInSpring()) {
            createZkProperty();
        }


        Environment environment = applicationContext.getEnvironment();
        if (!(environment instanceof ConfigurableEnvironment)) {
            return;
        }
        ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) environment;
        MutablePropertySources propertySources = configurableEnvironment.getPropertySources();
        PropertySource<?> propertySource = propertySources.get(zkName);

        ConcurrentHashMap<String, String> zkMap = (ConcurrentHashMap<String, String>) propertySource.getSource();

        try {
            List<String> propers = client.getChildren().forPath(path);
            for (String key : propers) {
                byte[] bytes = client.getData().forPath(path + "/" + key);
                zkMap.put(key, bytes != null ? new String(bytes, StandardCharsets.UTF_8) : "");
            }
        } catch (Exception e) {
            logger.error("Here is an exception：", e);
        }
    }

    private void createZkProperty() {

        Environment environment = applicationContext.getEnvironment();
        if (!(environment instanceof ConfigurableEnvironment)) {
            return;
        }
        ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) environment;
        MutablePropertySources propertySources = configurableEnvironment.getPropertySources();

        OriginTrackedMapPropertySource zkSource = new OriginTrackedMapPropertySource(zkName, map);
        propertySources.addFirst(zkSource);
    }

    private boolean chectExistsInSpring() {

        Environment environment = applicationContext.getEnvironment();
        if (!(environment instanceof ConfigurableEnvironment)) {
            return false;
        }
        ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment) environment;
        MutablePropertySources propertySources = configurableEnvironment.getPropertySources();
        for (PropertySource<?> propertySource : propertySources) {

            if (zkName.equalsIgnoreCase(propertySource.getName())) {
                return true;
            }
        }

        return false;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void destroy() throws Exception {
        scheduledExecutorService.shutdown();
        curatorFramework.close();
    }
}
